// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: jhkim, lucasyoo, junglekim

#include "coin2/strategy/cmm_strat/cmm_strategy.h"
#include "coin2/strategy/cmm_strat/cmm_factory.h"
#include <algorithm>
#include <glog/logging.h>


namespace coin2::strategy::cmm_strat {


CmmStrategy::CmmStrategy(const std::string& strat_json, std::optional<int64_t> ts)
    : strategy_created_time_(ts.has_value() ? *ts : GetCurrentTimestamp()) {
  std::ifstream in(strat_json);
  SPDLOG_INFO("Load strategy json: {}", strat_json);
  CHECK_THROW(in.is_open());
  strat_config_ = ::nlohmann::json::parse(in);
  dry_run_ = strat_config_["dry_run"];
  dry_report_ = strat_config_["dry_report"];
  for (const auto& sub_strat_config : strat_config_["subconfig"].items()) {
    auto strat = std::make_unique<CmmSubStrategy>(
        sub_strat_config.value(),
        dry_run_,
        strategy_created_time_);
    strats_.emplace(GetProductId(*strat->Pricer()->GetProductTarget()), std::move(strat));
  }
  if (strat_config_.contains("exclude_symbols")) {
    for (const auto& [acc, symbols] : strat_config_["exclude_symbols"].items()) {
      if (acc == "acct0") {
        for (const auto& symbol : symbols) {
          auto product = CreateProductFromUniqueString(symbol, strategy_created_time_);
          const auto& product_id = GetProductId(*product);
          exclude_products0_.push_back(std::move(product));
          last_exclude_balance0_[product_id] = std::nullopt;
          exclude_midp_[product_id] = std::nullopt;
        }
      }
      if (acc == "acct1") {
        for (const auto& symbol : symbols) {
          auto product = CreateProductFromUniqueString(symbol, strategy_created_time_);
          const auto& product_id = GetProductId(*product);
          exclude_products1_.push_back(std::move(product));
          last_exclude_balance1_[product_id] = std::nullopt;
          exclude_midp_[product_id] = std::nullopt;
        }
      }
    }
  }
}

void CmmStrategy::Init(Driver* driver) {
  driver_ = driver;
  for (const auto& strat : strats_) {
    strat.second->Init(driver);
  }
  if (!dry_report_) {
    const auto& request0 = driver->order()->GetUniqueSystemByName("acct0")->GetAccountRequest();
    const auto& request1 = driver->order()->GetUniqueSystemByName("acct1")->GetAccountRequest();
    strat_reporter_ = std::make_unique<StrategyReporter2>(driver->strategy()->config());
    for (auto& [_, strat] : strats_) {
      strat_reporter_->RegisterExecutorConfig(
          strategy_created_time_,
          request0,
          *strat->Pricer()->GetProductTarget(),
          strat->ExeConfig0());
      if (!strat->IsSameOwner()) {
        strat_reporter_->RegisterExecutorConfig(
          strategy_created_time_,
          request1,
          *strat->Pricer()->GetProductTarget(),
          strat->ExeConfig1());
      }
    }
    auto interval = driver->strategy()->config().pnl_report_period_ns();
    if (interval > 0) {
      pnl_monitor_ = std::make_unique<PnlMonitor>(
          driver->live_executor(),
          driver->archive_executor(),
          interval);
      pnl_monitor_->StartAsync();
    }
  }
}

void CmmStrategy::onBookFeed(const FeedUpdate& upd) {
  const auto product_id = GetProductId(upd.product());
  auto exclude_it = exclude_midp_.find(product_id);
  if (exclude_it != exclude_midp_.end() && upd.book().BidSize() > 0 && upd.book().AskSize() > 0) {
    exclude_midp_[product_id] = (upd.book().Bid0()->price + upd.book().Ask0()->price) / 2;
  }
  auto it = strats_.find(product_id);
  if (it == strats_.end() && exclude_it == exclude_midp_.end()) {
    SPDLOG_ERROR("strategy not found: {}", upd.product().base().symbol());
    return;
  }
  if (it != strats_.end()) it->second->onBookFeed(upd);
}

void CmmStrategy::onAccountInfo(const OrderUpdate& upd) {
  auto& account_info = upd.account_info();
  const int64_t& ts = upd.timestamp();
  if (!account_info.is_ready()) {
    SPDLOG_INFO("account not ready");
    return;
  }
  if (strat_reporter_) {
    const auto& request0 = driver_->order()->GetUniqueSystemByName("acct0")->GetAccountRequest();
    for (const auto& product : exclude_products0_) {
      if (account_info.HasBalance(product->base())) {
        const auto balance = account_info.GetBalance(product->base()).total();
        const auto product_id = GetProductId(*product);
        if (!last_exclude_balance0_.at(product_id).has_value() ||
            (last_exclude_balance0_.at(product_id).has_value() &&
             balance != last_exclude_balance0_.at(product_id).value())) {
          PassiveExecutorConfig new_config;
          new_config.set_reserve(balance);
          new_config.set_mea(account_info.mea().String());
          new_config.set_symbol(product->symbol());
          new_config.set_min_pos(balance / 2.0);
          new_config.set_max_pos(2 * balance);
          new_config.set_lot_size(balance / 3.0);
          strat_reporter_->RegisterExecutorConfig(ts, request0, *product, new_config);
          SPDLOG_INFO(
              "account0 excluded symbol {} balance change: {} -> {}",
              product->base().currency(),
              last_exclude_balance0_.at(product_id).value_or(0),
              balance);
          last_exclude_balance0_.at(product_id) = balance;
        }
        const auto& midp = exclude_midp_.at(product_id);
        if (midp.has_value()) strat_reporter_->UpdateMidp(*product, midp);
      }
    }
    const auto& request1 = driver_->order()->GetUniqueSystemByName("acct1")->GetAccountRequest();
    for (const auto& product : exclude_products1_) {
      if (account_info.HasBalance(product->base())) {
        const auto balance = account_info.GetBalance(product->base()).total();
        const auto product_id = GetProductId(*product);
        if (!last_exclude_balance1_.at(product_id).has_value() ||
            (last_exclude_balance1_.at(product_id).has_value() &&
             balance != last_exclude_balance1_.at(product_id).value())) {
          PassiveExecutorConfig new_config;
          new_config.set_reserve(balance);
          new_config.set_mea(account_info.mea().String());
          new_config.set_symbol(product->symbol());
          new_config.set_min_pos(balance / 2.0);
          new_config.set_max_pos(2 * balance);
          new_config.set_lot_size(balance / 3.0);
          strat_reporter_->RegisterExecutorConfig(ts, request1, *product, new_config);
          SPDLOG_INFO(
              "account1 excluded symbol {} balance change: {} -> {}",
              product->base().currency(),
              last_exclude_balance1_.at(product_id).value_or(0),
              balance);
          last_exclude_balance1_.at(product_id) = balance;
        }
        const auto& midp = exclude_midp_.at(product_id);
        if (midp.has_value()) strat_reporter_->UpdateMidp(*product, midp);
      }
    }
  }

  for (const auto& strat : strats_) {
    strat.second->onAccountInfo(upd);
  }
  if (strat_reporter_) {
    strat_reporter_->onAccountInfo(upd);
    for (const auto& strat : strats_) {
      const auto* prod = strat.second->Pricer()->GetProductTarget();
      double price = strat.second->Pricer()->GetMid().has_value()?
          strat.second->Pricer()->GetMid().value() : 0.0;
      if (price > 1e-8) strat_reporter_->UpdateMidp(*prod, price);
    }
  }
}

void CmmStrategy::onAccountOrder(const OrderUpdate& upd) {
  if (!upd.is_product_order_info()) return;
  auto it = strats_.find(GetProductId(upd.product()));
  if (it == strats_.end()) {
    SPDLOG_ERROR("strategy not found: {}", upd.product().base().symbol());
    return;
  }
  it->second->onAccountOrder(upd);
}

void CmmStrategy::onOrderLog(const coin::proto::OrderGatewayLog& order_log) {
  if (!order_log.has_account_request()) {
    return;
  }
  if (order_log.type() == coin::proto::OrderGatewayLog::ORDER_EVENT &&
      order_log.event().tag() != "aggressive" && order_log.event().tag() != "passive") {
    // filter out order event with tag "add_from_order_proto", foreign order
    return;
  }
  if (strat_reporter_) {
    strat_reporter_->GetMutableStrategyLogger()->WriteOrderLog(
        order_log,
        true,
        GetCurrentTimestamp());
  }
}

void CmmStrategy::onCleanup(Driver* driver) {
  for (const auto& strat : strats_) {
    strat.second->onCleanup(driver);
  }
}


CmmSubStrategy::CmmSubStrategy(
    const ::nlohmann::json& sub_strat_config,
    bool dry_run,
    std::optional<int64_t> ts)
    : sub_strat_config_(sub_strat_config),
      strategy_created_time_(ts.has_value() ? *ts : GetCurrentTimestamp()) {
  CHECK(google::protobuf::util::JsonStringToMessage(
            sub_strat_config_["oc_config"]["exec0"].dump(),
            &exe_config0_)
            .ok());
  CHECK(google::protobuf::util::JsonStringToMessage(
            sub_strat_config_["oc_config"]["exec1"].dump(),
            &exe_config1_)
            .ok());
  is_same_owner_ = sub_strat_config_["oc_config"]["owner"]["exec0"] ==
                   sub_strat_config_["oc_config"]["owner"]["exec0"];
  controller_.reset(new CmmOrderController(
      sub_strat_config_["oc_config"],
      strategy_created_time_,
      dry_run));
  pricer_ = CreateNewCmmPricer(sub_strat_config_["pricer_config"], strategy_created_time_);
}

void CmmSubStrategy::Init(Driver* driver) {
  driver_ = driver;
  controller_->InitExecutor(
      exe_config0_,
      exe_config1_,
      driver->order()->GetUniqueSystemByName("acct0"),
      driver->order()->GetUniqueSystemByName("acct1"),
      driver);
  GenerateOrderManagePeriod();
  // feed_timer_ = std::make_unique<DeadlineTimer>();
  // feed_timer_->ExpiresEvery(ordermanage_period_);
  boost::posix_time::nanoseconds interval(ordermanage_period_);
  ordermanage_timer_ = std::make_unique<boost::asio::deadline_timer>(
      *driver_->live_executor()->MainIoc(),
      interval);
  PeriodicOrderManage();
}

// Get recent fair price and make orders
void CmmSubStrategy::onFairPriceUpdate(int64_t timestamp) {
  std::pair<double, double> bbo = pricer_->GetFairPrice();
  controller_->onFairPriceUpdate(
      bbo.first,
      bbo.second,
      timestamp,
      pricer_->GetPI(),
      pricer_->GetProductTarget(),
      driver_);
}

// void CmmSubStrategy::onHeartbeatFeed(const FeedTimer& timer) {
//   onOrderManage(timer.Timestamp());
// }

void CmmSubStrategy::onBookFeed(const FeedUpdate& upd) {
  pricer_->onBookFeed(upd);
}

void CmmSubStrategy::onAccountInfo(const OrderUpdate& upd) {
  controller_->onAccountInfo(upd);
}

void CmmSubStrategy::onAccountOrder(const OrderUpdate& upd) {
  controller_->onAccountOrder(upd);
}

void CmmSubStrategy::onCleanup(Driver* driver) {
  SPDLOG_INFO("CmmStrategy::onCleanup Start");
  controller_->onCleanup0();
  if (!is_same_owner_) controller_->onCleanup1();
  SPDLOG_INFO("CmmStrategy::onCleanup End");
}


// Recursive Customcall: calling onOrderManage() every MaxTimeSleep seconds
void CmmSubStrategy::PeriodicOrderManage() {
  onOrderManage(GetCurrentTimestamp());
  GenerateOrderManagePeriod();  // Generate random sleep time for order
  ordermanage_timer_->expires_from_now(
      boost::posix_time::nanoseconds(ordermanage_period_));
  ordermanage_timer_->async_wait(std::bind(&CmmSubStrategy::PeriodicOrderManage, this));
}

// onOrderManage: to force orders even if book's not updated.
void CmmSubStrategy::onOrderManage(int64_t ts) {
  // auto current_time = GetCurrentTimestamp();
  onFairPriceUpdate(ts);
}


// Get random time for CleanupHandler in driver
void CmmSubStrategy::GenerateOrderManagePeriod() {
  auto& oc_config = controller_->GetOcConfig();
  double time_range = oc_config["time_range"];
  double avg_min = oc_config["avg_period_min"];
  std::uniform_real_distribution<double> dist(
      avg_min * (1 - time_range),
      avg_min * (1 + time_range));
  std::random_device rd;
  std::default_random_engine generator(rd());
  double period = dist(generator);
  ordermanage_period_ = static_cast<int64_t>(60 * period * 1'000'000'000L);
}


}  // namespace coin2::strategy::cmm_strat
